using System.Collections.Generic;
using System;
using Cysharp.Threading.Tasks;
using Geometry;
using Unity.Mathematics;
using System.Threading;
using System.Linq;
using EarthGeospatial;
using EarthUtility;
using Gltf;

namespace Cesium3DTilesSelection
{

    public class QuadtreeRasterOverlayTileProvider : RasterOverlayTileProvider
    {
        private const double pixelTolerance = 0.01;
        Rectangle _coverageRectangle;
        protected int MinimumLevel { get; set; }
        protected int MaximumLevel { get; set; }
        protected int ImageWidth { get; set; }
        protected int ImageHeight { get; set; }
        protected QuadtreeTilingScheme tilingScheme { get; set; }
        private Queue<CacheEntry> _tilesOldToRecent = new Queue<CacheEntry>();
        private Dictionary<QuadtreeTileID, List<CacheEntry>> _tileLookup = new Dictionary<QuadtreeTileID, List<CacheEntry>>();
        private Int64 _cachedBytes;

        class CacheEntry
        {
            public QuadtreeTileID tileID;
            public LoadedQuadtreeImage future;
        }

        struct LoadedQuadtreeImage
        {
            public LoadedRasterOverlayImage pLoaded;
            public Rectangle subset;
        }

        public QuadtreeRasterOverlayTileProvider(RasterOverlay owner, IPrepareRendererResources pPrepareRendererResources, Projection projection,
      QuadtreeTilingScheme tilingScheme, Rectangle coverageRectangle, int minimumLevel, int maximumLevel, int imageWidth, int imageHeight) : base(owner, pPrepareRendererResources, projection)
        {
            _coverageRectangle = coverageRectangle;
            MinimumLevel = minimumLevel;
            MaximumLevel = maximumLevel;
            ImageWidth = imageWidth;
            ImageHeight = imageHeight;
            this.tilingScheme = tilingScheme;
            _tilesOldToRecent = new Queue<CacheEntry>();
            _tileLookup = new Dictionary<QuadtreeTileID, List<CacheEntry>>();
            Interlocked.Exchange(ref _cachedBytes, 0);
        }


        protected virtual async UniTask<LoadedRasterOverlayImage> loadQuadtreeTileImage(QuadtreeTileID tileID)
        {
            throw new NotImplementedException();
        }
        protected override async UniTask<LoadedRasterOverlayImage> loadTileImage(RasterOverlayTile overlayTile)
        {

            List<LoadedQuadtreeImage> tiles = await this.mapRasterTilesToGeometryTile(overlayTile.Rectangle, overlayTile.TargetGeometricError);

            bool haveAnyUsefulImageData = tiles.Any(image => image.pLoaded.image != null && image.subset == null);
            if (!haveAnyUsefulImageData)
            {
                return new LoadedRasterOverlayImage();
            }

            var projection = this.pProjection;
            var rectangle = overlayTile.Rectangle;
            var images = tiles;
            return combineImages(rectangle, projection, images);
        }

        private LoadedRasterOverlayImage combineImages(Rectangle targetRectangle, EarthGeospatial.Projection projection, List<LoadedQuadtreeImage> images)
        {
            CesiumTextureUtility.loadTextureAnyThreadPart(images.First().pLoaded.image);
            CombinedImageMeasurements measurements = measureCombinedImage(targetRectangle, images);
            int targetImageBytes = measurements.widthPixels * measurements.heightPixels * measurements.channels * measurements.bytesPerChannel;
            if (targetImageBytes <= 0)
            {
                var nopixels = new LoadedRasterOverlayImage();
                nopixels.image = null;
                nopixels.rectangle = targetRectangle;
                nopixels.moreDetailAvailable = true;
                // Target image has no pixels, so our work here is done.
                return nopixels;
            }

            LoadedRasterOverlayImage result = new LoadedRasterOverlayImage();
            result.rectangle = measurements.rectangle;
            result.moreDetailAvailable = false;

            ImageCesium target = new ImageCesium();
            result.image = target;
            target.bytesPerChannel = measurements.bytesPerChannel;
            target.channels = measurements.channels;
            target.width = measurements.widthPixels;
            target.height = measurements.heightPixels;
            target.pixelData = new byte[target.width * target.height * target.channels * target.bytesPerChannel];

            foreach (var img in images)
            {
                LoadedRasterOverlayImage loaded = img.pLoaded;
                if (loaded.image == null)
                {
                    continue;
                }

                result.moreDetailAvailable |= loaded.moreDetailAvailable;
                blitImage(target, result.rectangle, loaded.image, loaded.rectangle, img.subset);
            }

            // size_t combinedCreditsCount = 0;
            // for (auto it = images.begin(); it != images.end(); ++it)
            // {
            //     const LoadedRasterOverlayImage&loaded = *it.pLoaded;
            //     if (!loaded.image)
            //     {
            //         continue;
            //     }

            //     combinedCreditsCount += loaded.credits.size();
            // }

            // result.credits.reserve(combinedCreditsCount);
            //     for (auto it = images.begin(); it != images.end(); ++it)
            //     {
            //         const LoadedRasterOverlayImage&loaded = *it.pLoaded;
            //         if (!loaded.image)
            //         {
            //             continue;
            //         }

            //         for (const Credit&credit : loaded.credits) {
            //         result.credits.push_back(credit);
            //     }
            // }

            return result;
        }
        struct CombinedImageMeasurements
        {
            public Rectangle rectangle;
            public int widthPixels;
            public int heightPixels;
            public int channels;
            public int bytesPerChannel;
        };
        private static CombinedImageMeasurements measureCombinedImage(Rectangle targetRectangle, List<LoadedQuadtreeImage> images)
        {
            double projectedWidthPerPixel = double.MaxValue;
            double projectedHeightPerPixel = double.MaxValue;
            int channels = -1;
            int bytesPerChannel = -1;
            foreach (LoadedQuadtreeImage image in images)
            {
                LoadedRasterOverlayImage loaded = image.pLoaded;
                if (loaded.image == null || loaded.image.width <= 0 || loaded.image.height <= 0)
                {
                    continue;
                }

                projectedWidthPerPixel = math.min(projectedWidthPerPixel, loaded.rectangle.computeWidth() / loaded.image.width);
                projectedHeightPerPixel = math.min(projectedHeightPerPixel, loaded.rectangle.computeHeight() / loaded.image.height);

                channels = math.max(channels, loaded.image.channels);
                bytesPerChannel = math.max(bytesPerChannel, loaded.image.bytesPerChannel);
            }

            Rectangle combinedRectangle = null;

            foreach (LoadedQuadtreeImage image in images)
            {
                LoadedRasterOverlayImage loaded = image.pLoaded;
                if (loaded.image == null || loaded.image.width <= 0 || loaded.image.height <= 0)
                {
                    continue;
                }

                // The portion of the source that we actually need to copy.
                Rectangle sourceSubset = image.subset ?? loaded.rectangle;

                // Find the bounds of the combined image.
                // Intersect the loaded image's rectangle with the target rectangle.
                Rectangle maybeIntersection = targetRectangle.computeIntersection(sourceSubset);
                if (!maybeIntersection)
                {
                    // We really shouldn't have an image that doesn't overlap the target.
                    UnityEngine.Debug.Assert(false);
                    continue;
                }

                Rectangle intersection = maybeIntersection;

                // Expand this slightly so we don't wind up with partial pixels in the
                // target
                intersection.minimumX = Mathd.roundDown(
                                            intersection.minimumX / projectedWidthPerPixel,
                                            pixelTolerance) *
                                        projectedWidthPerPixel;
                intersection.minimumY = Mathd.roundDown(
                                            intersection.minimumY / projectedHeightPerPixel,
                                            pixelTolerance) *
                                        projectedHeightPerPixel;
                intersection.maximumX = Mathd.roundUp(
                                            intersection.maximumX / projectedWidthPerPixel,
                                            pixelTolerance) *
                                        projectedWidthPerPixel;
                intersection.maximumY = Mathd.roundUp(
                                            intersection.maximumY / projectedHeightPerPixel,
                                            pixelTolerance) *
                                        projectedHeightPerPixel;

                // We always need at least a 1x1 image, even if the target uses a tiny
                // fraction of that pixel. e.g. if a level zero quadtree tile is mapped
                // to a very tiny geometry tile.
                if (intersection.minimumX == intersection.maximumX)
                {
                    intersection.maximumX += projectedWidthPerPixel;
                }
                if (intersection.minimumY == intersection.maximumY)
                {
                    intersection.maximumY += projectedHeightPerPixel;
                }

                if (combinedRectangle)
                {
                    combinedRectangle = combinedRectangle.computeUnion(intersection);
                }
                else
                {
                    combinedRectangle = intersection;
                }
            }

            if (!combinedRectangle)
            {
                return new CombinedImageMeasurements() { rectangle = targetRectangle };
            }

            // Compute the pixel dimensions needed for the combined image.
            int combinedWidthPixels = (int)(Mathd.roundUp(
                combinedRectangle.computeWidth() / projectedWidthPerPixel,
                pixelTolerance));
            int combinedHeightPixels = (int)(Mathd.roundUp(
                combinedRectangle.computeHeight() / projectedHeightPerPixel,
                pixelTolerance));

            return new CombinedImageMeasurements()
            {
                rectangle = combinedRectangle,
                widthPixels = combinedWidthPixels,
                heightPixels = combinedHeightPixels,
                channels = channels,
                bytesPerChannel = bytesPerChannel
            };
        }

        private void blitImage(ImageCesium target, Rectangle targetRectangle, ImageCesium source, Rectangle sourceRectangle, Rectangle sourceSubset)
        {
            Rectangle sourceToCopy = sourceSubset ?? sourceRectangle;
            Rectangle overlap = targetRectangle.computeIntersection(sourceToCopy);
            if (overlap == null)
            {
                return;
            }
            PixelRectangle targetPixels = computePixelRectangle(target, targetRectangle, overlap);
            PixelRectangle sourcePixels = computePixelRectangle(source, sourceRectangle, overlap);
            ImageManipulation.blitImage(target, targetPixels, source, sourcePixels);
        }

        private PixelRectangle computePixelRectangle(ImageCesium image, Rectangle totalRectangle, Rectangle partRectangle)
        {
            // Pixel coordinates are measured from the top left.
            // Projected rectangles are measured from the bottom left.

            int x = (int)(Mathd.roundDown(image.width * (partRectangle.minimumX - totalRectangle.minimumX) / totalRectangle.computeWidth(), pixelTolerance));
            x = math.max(0, x);
            int y = (int)(Mathd.roundDown(image.height * (totalRectangle.maximumY - partRectangle.maximumY) / totalRectangle.computeHeight(), pixelTolerance));
            y = math.max(0, y);

            int maxX = (int)(Mathd.roundUp(image.width * (partRectangle.maximumX - totalRectangle.minimumX) / totalRectangle.computeWidth(), pixelTolerance));
            maxX = math.min(maxX, image.width);
            int maxY = (int)(Mathd.roundUp(image.height * (totalRectangle.maximumY - partRectangle.minimumY) / totalRectangle.computeHeight(), pixelTolerance));
            maxY = math.min(maxY, image.height);

            return new PixelRectangle(x, y, maxX - x, maxY - y);
        }

        private async UniTask<List<LoadedQuadtreeImage>> mapRasterTilesToGeometryTile(Rectangle geometryRectangle, double targetGeometricError)
        {
            List<LoadedQuadtreeImage> result = new List<LoadedQuadtreeImage>();

            QuadtreeTilingScheme imageryTilingScheme = this.tilingScheme;

            // Use Web Mercator for our texture coordinate computations if this imagery
            // layer uses that projection and the terrain tile falls entirely inside the
            // valid bounds of the projection. bool useWebMercatorT =
            //     pWebMercatorProjection &&
            //     tileRectangle.getNorth() <= WebMercatorProjection::MAXIMUM_LATITUDE &&
            //     tileRectangle.getSouth() >= -WebMercatorProjection::MAXIMUM_LATITUDE;

            Rectangle providerRectangle = this.getCoverageRectangle();
            Rectangle tilingSchemeRectangle = imageryTilingScheme.getRectangle();

            // Compute the rectangle of the imagery from this raster tile provider that
            // overlaps the geometry tile.  The RasterOverlayTileProvider and its tiling
            // scheme both have the opportunity to constrain the rectangle.
            Rectangle imageryRectangle = tilingSchemeRectangle.computeIntersection(providerRectangle) ?? tilingSchemeRectangle;

            Rectangle intersection = new Rectangle(0.0, 0.0, 0.0, 0.0);
            Rectangle maybeIntersection = geometryRectangle.computeIntersection(imageryRectangle);
            if (maybeIntersection)
            {
                intersection = maybeIntersection;
            }
            else
            {
                // There is no overlap between this terrain tile and this imagery
                // provider.  Unless this is the base layer, no skeletons need to be
                // created. We stretch texels at the edge of the base layer over the entire
                // globe.

                // TODO: base layers
                // if (!this.isBaseLayer()) {
                //     return false;
                // }
                if (geometryRectangle.minimumY >= imageryRectangle.maximumY)
                {
                    intersection.minimumY = intersection.maximumY = imageryRectangle.maximumY;
                }
                else if (geometryRectangle.maximumY <= imageryRectangle.minimumY)
                {
                    intersection.minimumY = intersection.maximumY = imageryRectangle.minimumY;
                }
                else
                {
                    intersection.minimumY = math.max(geometryRectangle.minimumY, imageryRectangle.minimumY);

                    intersection.maximumY = math.min(geometryRectangle.maximumY, imageryRectangle.maximumY);
                }

                if (geometryRectangle.minimumX >= imageryRectangle.maximumX)
                {
                    intersection.minimumX = intersection.maximumX = imageryRectangle.maximumX;
                }
                else if (geometryRectangle.maximumX <= imageryRectangle.minimumX)
                {
                    intersection.minimumX = intersection.maximumX = imageryRectangle.minimumX;
                }
                else
                {
                    intersection.minimumX = math.max(geometryRectangle.minimumX, imageryRectangle.minimumX);

                    intersection.maximumX = math.min(geometryRectangle.maximumX, imageryRectangle.maximumX);
                }
            }

            // Compute the required level in the imagery tiling scheme.
            // TODO: dividing by 8 to change the default 3D Tiles SSE (16) back to the
            // terrain SSE (2)
            // TODO: Correct latitude factor, which is really a property of the
            // projection.
            int imageryLevel = this.computeLevelFromGeometricError(targetGeometricError / 8.0, intersection.getCenter());
            imageryLevel = math.max(0, imageryLevel);

            int maximumLevel = this.MaximumLevel;
            if (imageryLevel > maximumLevel)
            {
                imageryLevel = maximumLevel;
            }

            int minimumLevel = this.MinimumLevel;
            if (imageryLevel < minimumLevel)
            {
                imageryLevel = minimumLevel;
            }

            QuadtreeTileID? southwestTileCoordinatesOpt = imageryTilingScheme.positionToTile(intersection.getLowerLeft(), imageryLevel);
            QuadtreeTileID? northeastTileCoordinatesOpt =
                imageryTilingScheme.positionToTile(
                    intersection.getUpperRight(),
                    imageryLevel);

            // Because of the intersection, we should always have valid tile coordinates.
            // But give up if we don't.
            if (!southwestTileCoordinatesOpt.HasValue || !northeastTileCoordinatesOpt.HasValue)
            {
                return result;
            }

            QuadtreeTileID southwestTileCoordinates = southwestTileCoordinatesOpt.Value;
            QuadtreeTileID northeastTileCoordinates = northeastTileCoordinatesOpt.Value;

            // If the northeast corner of the rectangle lies very close to the south or
            // west side of the northeast tile, we don't actually need the northernmost or
            // easternmost tiles. Similarly, if the southwest corner of the rectangle lies
            // very close to the north or east side of the southwest tile, we don't
            // actually need the southernmost or westernmost tiles.

            // We define "very close" as being within 1/512 of the width of the tile.
            double veryCloseX = geometryRectangle.computeWidth() / 512.0;
            double veryCloseY = geometryRectangle.computeHeight() / 512.0;

            Rectangle southwestTileRectangle = imageryTilingScheme.tileToRectangle(southwestTileCoordinates);

            if (math.abs(southwestTileRectangle.maximumY - geometryRectangle.minimumY) <
                    veryCloseY &&
                southwestTileCoordinates.y < northeastTileCoordinates.y)
            {
                ++southwestTileCoordinates.y;
            }

            if (math.abs(southwestTileRectangle.maximumX - geometryRectangle.minimumX) <
                    veryCloseX &&
                southwestTileCoordinates.x < northeastTileCoordinates.x)
            {
                ++southwestTileCoordinates.x;
            }

            Rectangle northeastTileRectangle = imageryTilingScheme.tileToRectangle(northeastTileCoordinates);

            if (math.abs(northeastTileRectangle.maximumY - geometryRectangle.minimumY) <
                    veryCloseY &&
                northeastTileCoordinates.y > southwestTileCoordinates.y)
            {
                --northeastTileCoordinates.y;
            }

            if (math.abs(northeastTileRectangle.minimumX - geometryRectangle.maximumX) <
                    veryCloseX &&
                northeastTileCoordinates.x > southwestTileCoordinates.x)
            {
                --northeastTileCoordinates.x;
            }

            // Create TileImagery instances for each imagery tile overlapping this terrain
            // tile. We need to do all texture coordinate computations in the imagery
            // provider's projection.
            Rectangle imageryBounds = intersection;
            Rectangle clippedImageryRectangle = null;

            for (int i = southwestTileCoordinates.x; i <= northeastTileCoordinates.x;
                 ++i)
            {

                imageryRectangle = imageryTilingScheme.tileToRectangle(new
                    QuadtreeTileID(imageryLevel, i, southwestTileCoordinates.y));
                clippedImageryRectangle =
                    imageryRectangle.computeIntersection(imageryBounds);

                if (!clippedImageryRectangle)
                {
                    continue;
                }

                for (int j = southwestTileCoordinates.y;
                     j <= northeastTileCoordinates.y;
                     ++j)
                {

                    imageryRectangle = imageryTilingScheme.tileToRectangle(new QuadtreeTileID(imageryLevel, i, j));
                    clippedImageryRectangle = imageryRectangle.computeIntersection(imageryBounds);

                    if (!clippedImageryRectangle)
                    {
                        continue;
                    }

                    LoadedQuadtreeImage pTile = await this.getQuadtreeTile(new QuadtreeTileID(imageryLevel, i, j));
                    result.Add(pTile);
                }
            }

            return result;
        }

        private async UniTask<LoadedQuadtreeImage> getQuadtreeTile(QuadtreeTileID tileID)
        {
            //TODO 缓存处理
            // if (this._tileLookup.TryGetValue(tileID, out var lookupIt))
            // {
            //     // Move this entry to the end, indicating it's most recently used.
            //     this._tilesOldToRecent.splice(this._tilesOldToRecent.end(), this._tilesOldToRecent, cacheIt);

            //     return cacheIt.future;
            // }
            // We create this lambda here instead of where it's used below so that we
            // don't need to pass `this` through a thenImmediately lambda, which would
            // create the possibility of accidentally using this pointer to a
            // non-thread-safe object from another thread and creating a (potentially very
            // subtle) race condition.


            LoadedRasterOverlayImage loaded = await this.loadQuadtreeTileImage(tileID);
            if (loaded.image != null && loaded.errors != null &&
                loaded.image.width > 0 && loaded.image.height > 0)
            {
                // Successfully loaded, continue.
                Interlocked.Exchange(ref this._cachedBytes, loaded.image.pixelData.Length);
                return new LoadedQuadtreeImage() { pLoaded = loaded };
            }

            // Tile failed to load, try loading the parent tile instead.
            // We can only initiate a new tile request from the main thread,
            // though.
            var currentLevel = tileID.level;
            var minimumLevel = this.MinimumLevel;
            if (currentLevel > minimumLevel)
            {
                Rectangle rectangle = this.tilingScheme.tileToRectangle(tileID);
                QuadtreeTileID parentID = new QuadtreeTileID(tileID.level - 1, tileID.x >> 1, tileID.y >> 1);
                var parent = await getQuadtreeTile(parentID);
                return parent;
            }
            else
            {
                // No parent available, so return the original failed result.
                return new LoadedQuadtreeImage() { pLoaded = loaded };
            }


            // var newIt = new CacheEntry() {tileID=tileID,futur= new LoadedQuadtreeImage() };  
            // this._tilesOldToRecent.Enqueue(newIt);
            // this._tileLookup[tileID] = newIt;
            // this.unloadCachedTiles();

            // return newIt.future;
        }

        private void unloadCachedTiles()
        {
            throw new NotImplementedException();
        }

        private int computeLevelFromGeometricError(double geometricError, double2 position)
        {
            // PERFORMANCE_IDEA: factor out the stuff that doesn't change.
            var tilingScheme = this.tilingScheme;
            Rectangle tilingSchemeRectangle = tilingScheme.getRectangle();
            double toMeters = Projection.computeApproximateConversionFactorToMetersNearPosition(this.pProjection, position);

            double levelZeroMaximumTexelSpacingMeters = (tilingSchemeRectangle.computeWidth() * toMeters) /
               (this.ImageWidth * tilingScheme.getNumberOfXTilesAtLevel(0));

            double twoToTheLevelPower = levelZeroMaximumTexelSpacingMeters / geometricError;
            double level = math.log2(twoToTheLevelPower);
            double rounded = math.max(math.round(level), 0.0);
            return (int)(rounded);
        }



        private Rectangle getCoverageRectangle()
        {
            return _coverageRectangle;
        }
    }
}